﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.NetworkInformation;
using System.ServiceModel;
using System.ServiceModel.Description;
using InfiniTec.Diagnostics;
using InfiniTec.Exchange.Notifications.Microsoft.Exchange.WebServices;

namespace InfiniTec.Exchange.Notifications
{
	/// <summary>
	/// Implements a WCF service host which can be used to receive notification from an Exchange 2007 CAS role.
	/// </summary>
	[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
	public sealed class PushNotificationListener : INotificationServicePortType, IDisposable
	{
		private static readonly Trace _Trace = SubscriptionCollection.Trace;
		private readonly object _SubscriptionLock = new object();

		/// <summary>
		/// A reference to all subscriptions handled by this listener.
		/// </summary>
		private readonly Dictionary<string, WeakReference> _Subscriptions = new Dictionary<string, WeakReference>();

		private ServiceHost _Host;
		private string _LocalComputerDnsName;
		private Uri _LocalEndpoint;
		private int _Port = 80;
		private ConnectionSecurity _ConnectionSecurity = ConnectionSecurity.Unsecure;

		private string _RelativePath = string.Format(CultureInfo.InvariantCulture, "Temporary_Listen_Addresses/{0}",
		                                             Guid.NewGuid());

		/// <summary>
		/// Initializes a new instance of the <see cref="PushNotificationListener"/> class.
		/// </summary>
		public PushNotificationListener()
		{
			IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
			if (!string.IsNullOrEmpty(ipGlobalProperties.DomainName))
			{
				_LocalComputerDnsName = string.Format("{0}.{1}", ipGlobalProperties.HostName, ipGlobalProperties.DomainName);
			}
			else
			{
				_LocalComputerDnsName = ipGlobalProperties.HostName;
			}
		}

		/// <summary>
		/// Gets the local endpoint of this listener. A <see cref="Subscription"/> instance uses this address when registering with
		/// an Exchange 2007 server.
		/// </summary>
		/// <value>The local endpoint.</value>
		public Uri LocalEndpoint
		{
			get
			{
				EnsureServiceIsStarted("LocalEndpoint");
				return _LocalEndpoint;
			}
		}


		/// <summary>
		/// Gets a value indicating whether this instance is started.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is started; otherwise, <c>false</c>.
		/// </value>
		private bool IsStarted
		{
			get { return _Host != null; }
		}

		/// <summary>
		/// Gets or sets the port this listener runs on. Default value is 80.
		/// </summary>
		/// <value>The port.</value>
		public int Port
		{
			get { return _Port; }
			set
			{
				EnsureListenerIsNotStarted("Port");
				_Port = value;
			}
		}

		/// <summary>
		/// Gets or sets the relative path this listener uses on the selected <see cref="Port"/>. Defaults to <c>Temporary_Listen_Addresses/&lt;random guid&gt;</c>
		/// </summary>
		/// <value>The relative path of this listener.</value>
		public string RelativePath
		{
			get { return _RelativePath; }
			set
			{
				EnsureListenerIsNotStarted("RelativePath");
				_RelativePath = value;
			}
		}

		/// <summary>
		/// Gets or sets the DNS name of the local computer. Defaults to <see cref="Environment.MachineName"/>.
		/// </summary>
		/// <value>The DNS name of the local computer.</value>
		public string LocalComputerDnsName
		{
			get { return _LocalComputerDnsName; }
			set
			{
				EnsureListenerIsNotStarted("LocalComputerDnsName");
				_LocalComputerDnsName = value;
			}
		}

		/// <summary>
		/// Gets or sets the connection security for this listener. If set to <see cref="ConnectionSecurity"/>.Secure,
		/// an SSL certificate must be configured for the <see cref="Port"/>.
		/// </summary>
		/// <value>The connection security.</value>
		public ConnectionSecurity ConnectionSecurity
		{
			get { return _ConnectionSecurity; }
			set { _ConnectionSecurity = value; }
		}

		#region IDisposable Members

		/// <summary>
		/// Closes the WCF host.
		/// </summary>
		void IDisposable.Dispose()
		{
			if (_Host != null)
			{
				_Trace.Verbose("Closing WCF host for this SubscriptionCollection instance.");
				_Host.Close();
			}
		}

		#endregion

		#region INotificationServicePortType Members

		/// <summary>
		/// Implements the <see cref="INotificationServicePortType.SendNotification"/> method and routes notifications to their 
		/// <see cref="Subscription"/> instance responsible for the event.
		/// </summary>
		/// <param name="request">The request to process.</param>
		/// <returns>
		/// <list type="table">
		///		<item>
		///			<term><see cref="SubscriptionStatusType.OK"/></term>
		///			<description>When the subscription was handled successfully.</description>
		///		</item>
		///		<item>
		///			<term><see cref="SubscriptionStatusType.Unsubscribe"/></term>
		///			<description>When the subscription associated with the specified subscription id was not found.</description>
		///		</item>
		/// </list>
		/// </returns>
		SendNotificationResponse INotificationServicePortType.SendNotification(SendNotificationRequest request)
		{
			using (_Trace.StartOperation("Processing notification"))
			{
				foreach (ResponseMessageType messsage in request.SendNotification.ResponseMessages.Items)
				{
					if (messsage.ResponseCode != ResponseCodeType.NoError)
					{
						//TODO: Error handling
					}

					var notificationMessage = (SendNotificationResponseMessageType) messsage;
					var notification = notificationMessage.Notification;
					var subscriptionId = notification.SubscriptionId;

					_Trace.Verbose("Notification is for subscription with id {0}", subscriptionId);

					Subscription subscription;
					if (!TryResolveSubscription(subscriptionId, out subscription))
					{
						_Trace.Warning("Could not find an active subscription with id {0}. Cancelling subscription.");
						return CreateNotificationResult(SubscriptionStatusType.Unsubscribe);
					}
					subscription.ProcessEvents(notification.ConvertEvents());
				}
				return CreateNotificationResult(SubscriptionStatusType.OK);
			}
		}

		#endregion

		/// <summary>
		/// Associates the specified subscription with this instance.
		/// </summary>
		/// <param name="subscription">The subscription to associate this listener with.</param>
		public void Register(Subscription subscription)
		{
			if (subscription == null) throw new ArgumentNullException("subscription");
			lock (_SubscriptionLock)
			{
				_Subscriptions[subscription.Id] = new WeakReference(subscription);
			}
		}

		/// <summary>
		/// Closes the WCF host.
		/// </summary>
		public void Stop()
		{
			((IDisposable) this).Dispose();
		}

		private static SendNotificationResponse CreateNotificationResult(SubscriptionStatusType subscriptionStatus)
		{
			return new SendNotificationResponse
			       	{
			       		SendNotificationResult = new SendNotificationResultType
			       		                         	{
			       		                         		SubscriptionStatus = subscriptionStatus
			       		                         	}
			       	};
		}

		private bool TryResolveSubscription(string id, out Subscription subscription)
		{
			lock (_SubscriptionLock)
			{
				WeakReference subscriptionReference;
				if (_Subscriptions.TryGetValue(id, out subscriptionReference))
				{
					if (subscriptionReference.IsAlive)
					{
						subscription = (Subscription) subscriptionReference.Target;
						if (subscription != null)
						{
							return true;
						}
					}
					_Subscriptions.Remove(id);
				}
			}
			subscription = null;
			return false;
		}

		private void EnsureServiceIsStarted(string propertyName)
		{
			if (!IsStarted)
				throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
				                                                  "The property {0} cannot be accessed until the SubscriptionCollection has been started.",
				                                                  propertyName));
		}

		/// <summary>
		/// Starts this listener on the endpoint specified by the <see cref="LocalComputerDnsName"/>, <see cref="Port"/> and <see cref="RelativePath"/>.
		/// </summary>
		public void Start()
		{
			_Trace.Verbose("Starting SubscriptionCollection");
			try
			{

				string scheme;
				BasicHttpSecurityMode securityMode;
				ServiceMetadataBehavior metadataBehavior;
				switch (ConnectionSecurity)
				{
					case ConnectionSecurity.Unsecure:
						scheme = Uri.UriSchemeHttp;
						securityMode = BasicHttpSecurityMode.None;
						metadataBehavior = new ServiceMetadataBehavior { HttpGetEnabled = true };
						break;
					case ConnectionSecurity.Secure:
						scheme = Uri.UriSchemeHttps;
						securityMode = BasicHttpSecurityMode.Transport;
						metadataBehavior = new ServiceMetadataBehavior { HttpsGetEnabled = true };
						break;
					default:
						throw new InvalidOperationException("The ConnectionSecurityProperty must be set to either Secure or Unsecure.");
				}

				var ub = new UriBuilder
				         	{
				         		Scheme = scheme,
				         		Host = LocalComputerDnsName,
				         		Path = RelativePath,
				         		Port = Port
				         	};

				_LocalEndpoint = ub.Uri;

				_Trace.Verbose("Opening WCF Listener on {0}", _LocalEndpoint);

				_Host = new ServiceHost(this, _LocalEndpoint);
				_Host.AddServiceEndpoint(typeof (INotificationServicePortType),
				                         new BasicHttpBinding(securityMode),
				                         string.Empty);

				_Host.Description.Behaviors.Add(metadataBehavior);
				_Host.Open();
			}
			catch (Exception ex)
			{
				_Trace.Error("Failed to start SubscriptionCollection. Error: {0}", ex);
				throw;
			}
		}

		private void EnsureListenerIsNotStarted(string propertyName)
		{
			if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException("propertyName");

			if (IsStarted)
			{
				_Trace.Error("An attempt was made to modify property {0} although the service has already been started.",
				             propertyName);
				throw new InvalidOperationException(
					string.Format(CultureInfo.CurrentCulture, "Cannot modify the property {0} once the service has been started.",
					              propertyName));
			}
		}

		/// <summary>
		/// Removes the association between the specified subscription and this listener.
		/// </summary>
		/// <param name="subscription">The subscription.</param>
		public void Unregister(Subscription subscription)
		{
			if (subscription == null) throw new ArgumentNullException("subscription");
			lock (_SubscriptionLock)
			{
				if (_Subscriptions.ContainsKey(subscription.Id))
				{
					_Subscriptions.Remove(subscription.Id);
				}
			}
		}
	}
}